﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using net.windward.utils.ado;
using net.windward.api.csharp;
using Kailua.net.windward.utils;
using Kailua.net.windward.utils.datetime;
using System.Globalization;

namespace ArrowScheduleCore
{
    /// <summary>
    /// This schedules an item to run. RunLast and RunNext are in UTC time while RunTime is local time.
    /// </summary>
    public class ScheduleItem
    {
        private long scheduleId;
        private SchedulePeriod period;
        private string saveFolder;
        private string saveSiteUrl;
        private string emailTo;
        private readonly long timestamp;
        private readonly ScheduleVarItemCollection variables;
        private DateTime runLast; // UTC
        private string runResult;
        private DateTime runNext; // UTC

        /// <summary>
        /// Create the object to create a new event.
        /// </summary>
        public ScheduleItem()
        {
            scheduleId = WrConnectionParams.NULL_PK;
            Format = Report.REPORT_TYPE.PDF;
            variables = new ScheduleVarItemCollection();
            runLast = DateTime.MinValue;
            runNext = DateTime.MaxValue;
        }

        /// <summary>
        /// Create the object for an existing event.
        /// </summary>
        /// <param name="scheduleId">Primary key in the database.</param>
        /// <param name="title">Title of this event.</param>
        /// <param name="description">Description of this event. This is optional and can be null.</param>
        /// <param name="templateFileName">The template used to generate the report for this event.</param>
        /// <param name="format">The format for the generated reports.</param>
        /// <param name="period">The schedule for the report.</param>
        /// <param name="saveTitle">The title to use when saving this generated report. If this is null or 0 length then the generated 
        /// report is not saved. Either this and/or EmailTo must have a value.</param>
        /// <param name="saveNum">The number of reports to save. 0 is no limit.</param>
        /// <param name="emailTo">The email address(es) to send the report to. If this is null or 0 length then the generated report 
        /// is not emailed. Either this and/or SaveTitle must have a value.</param>
        /// <param name="timestamp">Last time the event was saved.</param>
        /// <param name="runLast">When this item was last run. DateTime.MinValue if never run. This is in UTC.</param>
        /// <param name="runResult">The status of the last run. Null if the run was successful. Setting this also sets the time of the run.</param>
        /// <param name="runNext">When to run next. This is set in CalculateRunNext(). This is in UTC.</param>
        public ScheduleItem(long scheduleId, string title, string description, string templateFileName,
                        Report.REPORT_TYPE format, SchedulePeriod period, string saveTitle, int saveNum, string emailTo,
                            long timestamp, DateTime runLast, string runResult, DateTime runNext)
        {
            this.scheduleId = scheduleId;
            Title = title;
            Description = description;
            TemplateFileName = templateFileName;
            Format = format;
            this.period = period;
            saveFolder = saveTitle;
            SaveNum = saveNum;
            this.emailTo = emailTo;
            this.timestamp = timestamp;
            this.runLast = runLast;
            this.runResult = runResult;
            this.runNext = runNext;
            variables = new ScheduleVarItemCollection();
        }

        /// <summary>
        /// Primary key in the database.
        /// </summary>
        public long Id
        {
            get
            {
                return scheduleId;
            }
        }

        /// <summary>
        /// Sets the primary key. Should only be called when this has been inserted into a database.
        /// </summary>
        /// <param name="id">The primary key.</param>
        public void setId(long id)
        {
            scheduleId = id;
        }

        /// <summary>
        /// Title of this event.
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// Description of this event. This is optional and can be null.
        /// </summary>
        public string Description { get; set; }

        /// <summary>
        /// <para>The template or report definition used to generate the report for this event.</para>
        /// <para>For arrow 3, this is the full url in sharepoint.</para>
        /// <para>NOTE: for arrow 3, when calling RunScheduledReport.Run, temporarily set 
        /// this value to the actual template name, and then restore the original value after the call 
        /// completes. The reason is that this value is used by RunScheduledReport.Run,
        /// and in that context, it must be the template's filename.</para>
        /// </summary>
        public string TemplateFileName { get; set; }

        /// <summary>
        /// SharePoint URL of template/report def.
        /// </summary>
        public String SiteUrl { get; set; }
        /// <summary>
        /// <para>System name.  Set to "arrow 3" for arrow 3.</para>
        /// </summary>
        /// <remarks>Used by <see cref="RunScheduledReport"/> to determine how to run various functions such
        /// as saving a file.</remarks>
        public string System { get; set; }
        /// <summary>
        /// SharePoint GUID of the datasource profile.
        /// </summary>
        public string DataProfileGuid { get; set; }
        /// <summary>
        /// <para>Deprecated. WindwardScheduler now determines this based on the extension of the
        /// file located at the URL in TemplateFileName. SchedulerWebPart and
        /// ConfigureReport always set this value to true.</para>
        /// <para>Indicates if the item uses a report definition (true) or template (false).</para>
        /// </summary>
        public bool IsRptAsm { get; set; }

        /// <summary>
        /// The format for the generated reports.
        /// </summary>
        public Report.REPORT_TYPE Format { get; set; }

        /// <summary>
        /// The schedule for the report.
        /// </summary>
        public SchedulePeriod Period
        {
            get { return period; }
            set { period = value; }
        }

        /// <summary>
        /// <para>Old Arrow: The title to use when saving this generated report. If this is null or 0 length then the generated report is not saved.
        /// Either this and/or EmailTo must have a value.</para>
        /// <para>Arrow 3: The SharePoint GUID of the folder to save the report in. Must be a valid GUID string. 
        /// Either this and/or EmailTo must have a value.</para>
        /// </summary>
        public string SaveFolder
        {
            get { return saveFolder; }
            set { saveFolder = value; }
        }

        /// <summary>
        /// The SharePoint URL of the web to save the report in.
        /// </summary>
        public string SaveSiteUrl
        {
            get { return saveSiteUrl; }
            set { saveSiteUrl = value; }
        }

        /// <summary>
        /// True if the generated report is saved.
        /// </summary>
        public bool IsSaveReport
        {
            get { return !string.IsNullOrEmpty(saveFolder); }
        }

        /// <summary>
        /// The number of reports to save. When a new one is saved, the oldest is deleted. Set to 0 if there is no limit.
        /// </summary>
        public int SaveNum { get; set; }

        /// <summary>
        /// The email address(es) to send the report to. If this is null or 0 length then the generated report is not emailed.
        /// Either this and/or SaveTitle must have a value.
        /// </summary>
        public string EmailTo
        {
            get { return emailTo; }
            set { emailTo = value; }
        }

        /// <summary>
        /// True if the generated report is emailed.
        /// </summary>
        public bool IsEmailTo
        {
            get { return !string.IsNullOrEmpty(emailTo); }
        }

        /// <summary>
        /// Last time the event was saved.
        /// </summary>
        public long Timestamp
        {
            get { return timestamp; }
        }

        /// <summary>
        /// The datasource map vars for this event.
        /// </summary>
        public ScheduleVarItemCollection Variables
        {
            get { return variables; }
        }

        /// <summary>
        /// When this item was last run. DateTime.MinValue if never run. This is in UTC.
        /// </summary>
        public DateTime RunLast
        {
            get { return runLast; }
        }

        /// <summary>
        /// When this item was last run. "{not run}" if never run.
        /// </summary>
        public string RunLastText
        {
            get
            {
                return runLast == DateTime.MinValue ? GlobalStrings.ScheduleItem_schdItemNotRun :
                runLast.ToLocalTime().ToShortDateString() + " " + runLast.ToLocalTime().ToShortTimeString();
            }
        }

        /// <summary>
        /// The status of the last run. Null if the run was successful. Setting this also sets the time of the run.
        /// </summary>
        public string RunResult
        {
            get { return runResult; }
            set
            {
                runLast = DateTime.UtcNow;
                runResult = value;
            }
        }

        /// <summary>
        /// When to run next. This is set in CalculateRunNext(). This is in UTC.
        /// </summary>
        public DateTime RunNext
        {
            get { return runNext; }
        }

        /// <summary>
        /// When this item will next be run - in local time.
        /// </summary>
        public string RunNextLocal
        {
            get { return runNext.ToLocalTime().ToShortDateString() + " " + runNext.ToLocalTime().ToShortTimeString(); }
        }

        /// <summary>
        /// Determine when to next run this event. RunNext is in UTC.
        /// </summary>
        public void CalculateRunNext()
        {

            Trap.trap(period.RunTime.Kind != DateTimeKind.Local);
            Trap.trap((runLast.Date != DateTime.MinValue.Date) && (runLast.Kind != DateTimeKind.Utc));
            Trap.trap((runNext.Kind != DateTimeKind.Utc) && (runNext.Date != DateTime.MaxValue.Date));

            DateTime now = DateTime.Now;
            now = new DateTime(now.Year, now.Month, now.Day, now.Hour, (now.Minute / 15) * 15, 0, DateTimeKind.Local);
            DateTime localRunNext; // local time
            // It can be today if never run and before time.
            if (runLast == DateTime.MinValue)
            {
                // get it to today
                localRunNext = period.RunTime;
                if (localRunNext.Date < now.Date)
                    localRunNext = new DateTime(now.Year, now.Month, now.Day, period.RunTime.Hour, period.RunTime.Minute, period.RunTime.Second, DateTimeKind.Local);
                // if now after start time, tomorrow
                if ((localRunNext.Date == now.Date) && (localRunNext.TimeOfDay < now.TimeOfDay))
                    localRunNext = localRunNext.AddDays(1);
            }
            else
            {
                // the earliest is the next day (from when run) if we've just run. 
                // or runTime if that is later (happens when editing time).
                // set to day after runLast at the correct time.
                localRunNext = period.RunTime > runLast ? period.RunTime : runLast.ToLocalTime().AddDays(1);
                localRunNext = new DateTime(localRunNext.Year, localRunNext.Month, localRunNext.Day, period.RunTime.Hour, period.RunTime.Minute, period.RunTime.Second, DateTimeKind.Local);
            }
            // it is still possibly before today but it is at least 1 day after runLast so it is on a time when it could be run.
            DateTime runMin = localRunNext;

            // if we are still in the past, get the number of days to catch up. If runNext is today but before now, skip to tomorrow
            int days = (localRunNext.Date < now.Date) ? (now.Date - localRunNext.Date).Days : 0;
            if ((localRunNext.AddDays(days).Date == now.Date) && (localRunNext.TimeOfDay < now.TimeOfDay))
                days++;
            localRunNext = localRunNext.AddDays(days);

            // runNext is today (> now) or tomorrow - skip it up based on when it should run.
            switch (period.RepeatType)
            {
                case SchedulePeriod.REPEAT.DAILY:
                    ScheduleDaily daily = (ScheduleDaily)period;
                    if (daily.Cycle == ScheduleDaily.CYCLE.WEEKDAYS)
                    {
                        while (DateTimeUtils.IsWeekend(period.Culture, localRunNext.DayOfWeek))
                            localRunNext = localRunNext.AddDays(1);
                    }
                    else
                    {
                        // skip to the next Nth day
                        localRunNext = localRunNext.AddDays(daily.Period - 1);
                        // make sure it's N days after start
                        if (runLast != DateTime.MinValue)
                            localRunNext = localRunNext.AddDays(-(localRunNext.Date - runLast.ToLocalTime().Date).Days % daily.Period);
                    }
                    break;

                case SchedulePeriod.REPEAT.WEEKLY:
                    // DayOfWeek is 0..6 with Sunday==0; DAYS is 0x01..0x40 with Sunday==0x01
                    ScheduleWeekly weekly = (ScheduleWeekly)period;
                    // if we are on a skipped week, bring it up to the start of the first good week.
                    if (weekly.Period > 1)
                    {
                        int weeks = (localRunNext.Date.AddDays(-(int)localRunNext.DayOfWeek) -
                                     period.RunTime.Date.AddDays(-(int)period.RunTime.DayOfWeek)).Days / 7;
                        weeks += weekly.Period - 1;
                        weeks -= weeks % weekly.Period;
                        DateTime first = period.RunTime.AddDays(weeks * 7 - (int)period.RunTime.DayOfWeek);
                        if (first > localRunNext)
                            localRunNext = first;
                    }

                    // go through rest of this week looking for a day. If don't have any more, end up on day 0 of next week.
                    // if on a skipped week, move to the first day of a run week.
                    // if we have additional days this week - get them.
                    bool foundIt = false;
                    do
                    {
                        ScheduleWeekly.DAYS dayFlag = (ScheduleWeekly.DAYS)(0x01 << (int)localRunNext.DayOfWeek);
                        if ((weekly.Days & dayFlag) != 0)
                        {
                            foundIt = true;
                            break;
                        }
                        localRunNext = localRunNext.AddDays(1);
                    }
                    while (localRunNext.DayOfWeek != DayOfWeek.Sunday);
                    if (foundIt)
                        break;
                    // new week - jump if interval greater than every week (to get us to the next Nth week)
                    if (weekly.Period > 1)
                        localRunNext = localRunNext.AddDays(7 * (weekly.Period - 1));
                    // first matching day we run on - including today
                    while (localRunNext.DayOfWeek != DayOfWeek.Saturday)
                    {
                        ScheduleWeekly.DAYS dayFlag = (ScheduleWeekly.DAYS)(0x01 << (int)localRunNext.DayOfWeek);
                        if ((weekly.Days & dayFlag) != 0)
                            break;
                        localRunNext = localRunNext.AddDays(1);
                    }
                    break;

                case SchedulePeriod.REPEAT.MONTHLY:
                    ScheduleMonthly monthly = (ScheduleMonthly)period;
                    // lets get to the right month without worrying about if the day is ok
                    if (monthly.Period > 1)
                    {
                        // set to 1st day of the month to avoid Feb 31 problems
                        int months = localRunNext.Year == period.RunTime.Year ? localRunNext.Month - period.RunTime.Month :
                                     (12 - period.RunTime.Month) + ((localRunNext.Year - period.RunTime.Year - 1) * 12) + localRunNext.Month;
                        months += monthly.Period - 1;
                        months -= months % monthly.Period;
                        DateTime first = period.RunTime.AddDays(1 - period.RunTime.Day).AddMonths(months);
                        if (first > localRunNext)
                            localRunNext = first;
                    }
                    int day = GetDay(period.Culture, monthly, localRunNext);
                    localRunNext = localRunNext.AddDays(day - localRunNext.Day);
                    if (localRunNext >= runMin)
                        break;
                    // we're past it this month. Jump to the next.
                    localRunNext = localRunNext.AddDays(1 - localRunNext.Day).AddMonths(monthly.Period);
                    localRunNext = localRunNext.AddDays(GetDay(period.Culture, monthly, localRunNext) - 1);
                    break;

                case SchedulePeriod.REPEAT.YEARLY:
                    ScheduleYearly yearly = (ScheduleYearly)period;
                    if (yearly.Cycle == ScheduleMonth.CYCLE.DAY)
                    {
                        localRunNext = new DateTime(localRunNext.Year, (int)yearly.Month, yearly.Day, localRunNext.Hour,
                                                    localRunNext.Minute, localRunNext.Second, DateTimeKind.Local);
                        if (localRunNext < runMin)
                            localRunNext = localRunNext.AddYears(1);
                        break;
                    }

                    localRunNext = new DateTime(localRunNext.Year, (int)yearly.Month, GetDay(period.Culture, yearly, localRunNext),
                                                localRunNext.Hour, localRunNext.Minute, localRunNext.Second, DateTimeKind.Local);
                    if (localRunNext < runMin)
                        localRunNext = localRunNext.AddYears(1);
                    break;
            }

            // if still before now, make it now
            if (localRunNext < now)
            {
                Trap.trap();
                localRunNext = now;
            }

            // and we now convert RunNext to UTC - it was local through all of above
            // we have to convert to UTC using the RunZone, not the CurrentTimeZone which is what localRunNext is
            Trap.trap(localRunNext.Kind != DateTimeKind.Local);
            //runNext = Period.RunZone.ToUniversalTime(localRunNext);
            runNext = TimeZoneInfo.ConvertTimeToUtc(localRunNext);
        }

        private static int GetDay(CultureInfo ci, ScheduleMonth monthly, DateTime date)
        {

            int lastDay = ci.Calendar.GetDaysInMonth(date.Year, date.Month);
            if (monthly.Cycle == ScheduleMonth.CYCLE.DAY)
                return Math.Min(monthly.Day, lastDay);

            int num = (int)monthly.Week;
            DayOfWeek[] okDays;
            switch (monthly.Weekday)
            {
                case ScheduleMonth.WEEKDAY.FIRST_DAY:
                    return Math.Min(num * 7 + 1, lastDay);
                case ScheduleMonth.WEEKDAY.FIRST_WEEKDAY:
                    okDays = new DayOfWeek[5];
                    int pos = 0;
                    for (int day = 0; day < 7; day++)
                        if (!DateTimeUtils.IsWeekend(ci, (DayOfWeek)day))
                            okDays[pos++] = (DayOfWeek)day;
                    break;
                case ScheduleMonth.WEEKDAY.FIRST_WEEKEND:
                    okDays = new DayOfWeek[2];
                    int pos2 = 0;
                    for (int day = 0; day < 7; day++)
                        if (DateTimeUtils.IsWeekend(ci, (DayOfWeek)day))
                            okDays[pos2++] = (DayOfWeek)day;
                    break;
                default:
                    okDays = new DayOfWeek[1];
                    okDays[0] = (DayOfWeek)((int)monthly.Weekday - (int)ScheduleMonth.WEEKDAY.SUNDAY);
                    break;
            }

            // jump to the start of this week.
            num = num * 7 + 1;
            Trap.trap(num == lastDay);
            date = num > lastDay ? date.AddDays(lastDay - date.Day) : date.AddDays(num - date.Day);
            int direction = 1;
            while (!IsDayOfWeek(okDays, date.DayOfWeek))
            {
                if (date.Day >= lastDay)
                    direction = -1;
                date = date.AddDays(direction);
            }
            return date.Day;
        }

        private static bool IsDayOfWeek(IEnumerable<DayOfWeek> okDays, DayOfWeek dayOfWeek)
        {
            return okDays.Any(day => day == dayOfWeek);
        }

        #region test only

        /// <summary>
        /// For testing only!!!
        /// </summary>
        /// <param name="time">The time to set RunLast to. In UTC</param>
        public void _testSetRunLast(DateTime time)
        {
            runLast = time;
        }

        /// <summary>
        /// For testing only!!!
        /// </summary>
        /// <param name="time">The time to set RunNext to. In UTC</param>
        public void _testSetRunNext(DateTime time)
        {
            runNext = time;
        }

        #endregion
    }

    /// <summary>
    /// A collection of ScheduleItem objects.
    /// </summary>
    public class ScheduleItemCollection : Collection<ScheduleItem>
    {
        /// <summary>
        /// Find by the primary key.
        /// </summary>
        /// <param name="id">The primary key.</param>
        /// <returns>The requested ScheduleItem.</returns>
        public ScheduleItem FindById(long id)
        {
            return this.FirstOrDefault(item => item.Id == id);
        }
    }
}
